40. Multiple Inheritance

Multiple Inheritance: MI
C++은 Java와 달리 다중 상속을 허용한다.
다중 상속은 둘 이상의 기본 클래스로부터 동일한 이름을 물려받을 가능성을 만든다.(함수, typedef 등)
다중 상속으로 인해 모호성이 생김
class BorrowableItem{
public:
void checkOut();
// ...
};
class ElectronicGadget{
private:
bool checkOut() const;
// ...
};
class MP3Player: public BorrowableItem, private ElectronicGadget{
// ...
}; //
MP3Player mp;
mp.checkOut(); //
mp.BorrowableItem::checkOut(); //
위에서 MP3Player는 BorrowableItem을 public으로, ElectronicGadget을 private으로 상속했기 때문에
mp.chechkOut으로 접근할 수 있는 함수는 하나 밖에 없다.
하지만, C++ 컴파일러는 어떤 함수가 접근 가능한 함수인지 알아보기 전에
중복된 함수 호출 중 하나를 골라낸다.

호출에 대해 최적으로 일치하는(best-match) 함수를 골라낸다(이 후, 접근 가능성 점검)
죽음의 MI 마름모꼴(deadly MI diamond)
class File{/* ... */};
class InputFile: public File{/* ... */};
class OutputFile: public File{/* ... */};
class IOFile: public InputFile, public OutputFile{/* ... */};
위와 같이 기본 클래스와 파생 클래스 사이의 경로가 두 개 이상이 되는 상속 구조는
경로의 개수만큼 기본 클래스의 데이터 멤버가 중복 생성된다.

멤버의 중복 생성을 막기 위해서 해당 데이터 멤버를 가진 클랫를 가상 기본 클래스로 정의한다.
가상 기본 클래스로 삼을 클래스에 직접 연결된 파생 클래스에서 가상 상속(virtual inheritance)를 사용
class File{/*...*/};
class InputFile: virtual public File{/*...*/};
class OutputFile: virtual public File{/*...*/};
class IOFile: public InputFile, public OutputFile{/*...*/};
표준 C++ 라이브러리 basic_ios, basic_istream, basic_ostream, basic_iostream 도 위와 같이 구현

정확한 동작 관점에서 모든 public 상속은 가상 상속하는 것이 맞다.
하지만, 내부적으로 가상 상속은 동작을 위해 더 많은 공간을 차지하며, 접근 속도도 더 느리다.
특히, 가상 기본 클래스의 초기화 관련 규칙은 비가상 기본 클래스의 초기화 규칙보다 훨씬 복잡하다.

비가상 상속을 기본으로 사용하되,
가상 상속을 반드시 해야 하는 경우라면, 가상 기본 클래스에 데이터가 들어가지 않도록 한다.
(자바의 인터페이스는 다중 상속을 지원한다. 또한 자바는 인터페이스에 데이터를 가지지 못하게 되어 있다.)

가상 상속은 자바의 인터페이스에 대한 Implement와 유사함
인터페이스 클래스를 이용한 다중 상속
class IPerson{
public:
virtual ~IPerson();
virtual std::string name() const=0;
virtual std::string birthDate() const=0;
};
//
std::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);
// IPerson ,
DatabaseID id(askUserForDatabaseID();
std::shared_ptr<IPerson> pp(makePerson(id)); //
// CPerson PersonInfo
class PersonInfo{
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName() const;
virtual const char* theBirthDate() const;
// ...
private:
virtual const char* valueDelimOpen() const;
virtual const char* valueDelimClose() const;
// ...
};
const char* PersonInfo::valueDelimOpen() const{
return "[";
}
const char* PersonInfo::valueDelimClose() const{
return "]";
}
const char* Personinfo::name() const{
// ( 0 )
static char value[MAX_Formatted_Field_Value_Length];
// !
std::strcpy(value, valueDelimOpen());
// name
std::strcat(value, valueDelimClose());
return value;
}
// PersonInfo Delim '[', ']' CPerson
class CPerson: public IPerson, private PersonInfo{
public:
explicit CPerson(DatabaseID pid): PersonInfo(pid) {}
virtual std::string name() const{ // private PersonInfo
return PersonInfo::theName();
}
virtual std::string birthDate() const{
return PersonInfo::theBirtheDate();
}
private:
const char* valueDelimOpen() const{ return ""; } //
const char* valueDelimOpen() const{ return ""; }
}
인터페이스로 인스턴스(객체)를 생성할 수 없기 떄문에,
인터페이스의 멤버를 사용하기 위해서는 포인터 및 참조자를 이용해야 한다.

일반적으로 다중 상속 시, 인터페이스는 public 상속을 하고, 일반 클래스(구현 클래스)는 private으로 상속한다.